<?php

/**
 * RDFIOSMWBatchWriter can take data in different formats (RDF/XML, SPARQL query 
 * structure etc) and perform write operations on the resulting wiki articles by 
 * calling SMWWriter
 * @author samuel.lampa@gmail.com
 * @package RDFIO 
 */
class RDFIOSMWBatchWriter {
    protected $m_delete;
    protected $m_unparseddata;
    protected $m_dataformat;
    protected $m_parser;
    protected $m_equivurihandler;
    protected $m_triples;
    protected $m_tripleindex;
    protected $m_tripleindexflat;
    protected $m_importedtriples;
    protected $m_pages;
    protected $m_errors;
    protected $m_haserrors;
    
    function __construct( $importdata, $dataformat ) {
        if ( $dataformat == 'triples_array' ) {
            $this->m_triples = $importdata;
        } else {
            $this->m_unparseddata = $importdata;
        }
        $this->m_dataformat = $dataformat;
        $this->m_haserrors = false;
        $this->m_delete = false;
    }

    /**
     * Execute Import of data sent to RDFImporter object upon creation.
     */
    public function execute() {
        if ( $this->m_dataformat == 'rdfxml' ) {
            $this->m_unparseddata = $this->cleanupXML( $this->m_unparseddata );
            $this->m_parser = ARC2::getRDFXMLParser();
            $this->parse();
        } else if ( $this->m_dataformat == 'turtle' ) {
            $this->m_unparseddata = $wgRequest->getText('importdata');
            $this->m_parser = ARC2::getTurtleParser();
            $this->parse();
        } 
        $this->extractTripleIndex();
        $this->m_equivurihandler = new RDFIOEquivalentURIHandler;
        $this->m_equivurihandler->setNSPrefixes( $this->getNSPrefixMapping() );
        $this->m_equivurihandler->setTripleIndex( $this->m_tripleindex );
        $this->preparePageHandlers();
        if ( $this->m_delete ) {
            $this->deletePageDataFromWiki();
        } else {
            $this->writePagesToWiki();
        }
    }
    
    /**
     * Delete imported facts instead of adding them
     */
    public function executeDelete() {
        $this->m_delete = true;
        $this->execute();
        $this->m_delete = false;
    }

    /**
     * Parse the imported data into ARC2 data structures which are stored in object variables.
     */
    private function parse() {
        $this->m_parser->parse( '', $this->m_unparseddata );
        $this->extractTriples();
    }

    /**
     * Prepare page handlers, which represent pages wiki pages to be written to/deleted from,
     * and the corresponding facts to add/remove
     */
    private function preparePageHandlers() {
        // Add type info to the Original URI property
        $property_hastypeurl = array( array( 'p' => 'Has type', 'v' => 'URL') );
        $origuripage = new RDFIOPageHandler( 'Original URI', SMW_NS_PROPERTY, $property_hastypeurl, $this->m_equivurihandler );
        $this->addToPages( $origuripage );
        
        // Prepare for storing the data to write in internal data structure ($this->m_pages)
        $unique_subject_uris = $this->getUniqueSubjectURIs();
        foreach ( $unique_subject_uris as $subject_uri ) {
            $wikititle = $this->m_equivurihandler->getWikiTitleForURI( $subject_uri );
            $triplesforpage = $this->getTriplesForSubject( $subject_uri );
            $properties = array();
            $i = 0;
            foreach ( $triplesforpage as $triple ) {
                $propertyuri = $triple['p'];
                $valueorig = $triple['o'];
                $valuetype = $triple['o_type'];
                $property = $this->m_equivurihandler->getWikiTitleForURI( $propertyuri );
                if ( $valuetype == 'uri' ) {
                    $value = $this->m_equivurihandler->getWikiTitleForURI( $valueorig );
                } else {
                    $value = $valueorig;
                }
                $properties[$i] = array( 'p' => $property, 'v' => $value );
                $i++;
            }
            // A URIResolver URI indicates that internal titles only are used and we have no Original URI available
            if ( !RDFIOUtils::isURIResolverURI( $subject_uri ) && !RDFIOUtils::isArcUntitledNode( $subject_uri ) ) {
                // Add the Original URI fact to the list of properties
                $properties[$i] = array( 'p' => 'Original URI', 'v' => $subject_uri );
                $i++;
            }
            $this->addToPages( new RDFIOPageHandler( $wikititle, NS_MAIN, $properties, $this->m_equivurihandler ) );
        }

        // Prepare property pages
        $unique_property_uris = $this->getUniquePropertyURIs();
        foreach ( $unique_property_uris as $property_uri => $property_uridata ) {
            $wikititle = $this->m_equivurihandler->getWikiTitleForURI( $property_uri );

            $type = $this->convertARCTypeToSMWType( $property_uridata['type'], $property_uridata['datatype'] );
            $property_hastype = array( 'p' => 'Has type', 'v' => $type );
            // A URIResolver URI indicates that internal titles only are used and we have no Original URI available
            if ( !RDFIOUtils::isURIResolverURI( $property_uri ) && !RDFIOUtils::isArcUntitledNode( $wikititle ) ) {
                $property_origuri = array( 'p' => 'Original URI', 'v' => $property_uri );
                $properties = array( $property_origuri, $property_hastype );
            } else {
                $properties = array( $property_hastype );
            }
            $propertypage = new RDFIOPageHandler( $wikititle, SMW_NS_PROPERTY, $properties, $this->m_equivurihandler );
            $this->addToPages( $propertypage );
        }
        
        // Prepare value pages
        // TODO: Look for a way to merge with the above code, or otherwise refactor ...
        $unique_value_uris = $this->getUniqueValueURIs();
        foreach ( $unique_value_uris as $unique_value_uri => $unique_value_uridata ) {
            $wikititle = $this->m_equivurihandler->getWikiTitleForURI( $unique_value_uri );

            // A URIResolver URI indicates that internal titles only are used and we have no Original URI available
            if ( !RDFIOUtils::isURIResolverURI( $unique_value_uri ) && !RDFIOUtils::isArcUntitledNode( $unique_value_uri ) ) {
                $value_origuri = array( 'p' => 'Original URI', 'v' => $unique_value_uri );
                $values = array( $value_origuri );
            } 
            $valuepage = new RDFIOPageHandler( $wikititle, NS_MAIN, $values, $this->m_equivurihandler );
            $this->addToPages( $valuepage );
        }
    }
    
    /**
     * Write the pages, with corresponding facts, represented as page handlers, to the wiki
     */
    private function writePagesToWiki() {
        global $wgOut;
        
        $pages = $this->m_pages;
        foreach ( $pages as $page ) {
            $page->writeDataToWiki();
            if ( $page->hasErrors() ) {
                $errortitle = "Error for wikipage \"" . $page->getWikiTitleFull() . "\"</h3>";
                $wgOut->addHTML( RDFIOUtils::formatErrorHTML( $errortitle, $page->getErrorText() ));
            }
        }
    }
    
    /**
     * Delete the facts stored in page handlers, from the wiki
     */
    private function deletePageDataFromWiki() {
        global $wgOut;
        
        $pages = $this->m_pages;
        foreach ( $pages as $page ) {
            $page->deleteDataFromWiki();
            if ( $page->hasErrors() ) {
                $errortitle = "Error for wikipage \"" . $page->getWikiTitleFull() . "\"</h3>";
                $wgOut->addHTML( RDFIOUtils::formatErrorHTML( $errortitle, $page->getErrorText() ));
            }
        }
    }

    /**
     * Get generated triples from the ARC2 parser and store in object variable
     */
    private function extractTriples() {
        $this->m_triples = $this->m_parser->getTriples();
    }

    /**
     * Get generated triple index from the ARC2 parser and store in object variable
     */
    private function extractTripleIndex() {
        $this->m_tripleindex = ARC2::getSimpleIndex( $this->m_triples, false );
        $this->m_tripleindexflat = ARC2::getSimpleIndex( $this->m_triples, true );
    }
    
    /**
     * Add a page handler to the current SMWBatchWriter
     * @param RDFIOPageHandler $page
     */
    private function addToPages( $page ) {
        $this->m_pages[count($this->m_pages)] = $page;
    }

    /**
     * Get all triples with a given subject
     * @param string $subjecturi
     * @return array
     */
    function getTriplesForSubject( $subjecturi ) {
        $reconstructedIndex = array( $subjecturi => $this->m_tripleindex[$subjecturi]);
        return ARC2::getTriplesFromIndex( $reconstructedIndex );
    }

    /**
     * Get a list of unique subject URI:s from the internally stored triple index
     * @return array
     */
    function getUniqueSubjectURIs() {
        return array_keys( $this->m_tripleindex );
    }

    /**
     * Get a list of unique property URI:s and corresponding type info, from the
     * internally stored triple index
     * @return array
     */
    function getUniquePropertyURIs() {
        $tripleindex = $this->m_tripleindex;
        $properties = array();
        foreach( $tripleindex as $cur_props ) {
            foreach( $cur_props as $cur_prop => $cur_propdata ) {
                $properties[$cur_prop] = array( 
                	'type' => $cur_propdata[0]['type'], 
                	'datatype' => $cur_propdata[0]['datatype'] 
                ); // Only the type info is interesting here
            }
        }
        return $properties;
    }
    
    /**
     * Get an array of unique URIs occuring as values in imported data
     * @return array $value_uris
     */
    function getUniqueValueURIs() {
        $tripleindex = $this->m_tripleindex;
        $value_uris = array();
        foreach( $tripleindex as $cur_props ) {
            foreach( $cur_props as $cur_prop => $cur_propdatas ) {
                foreach ( $cur_propdatas as $cur_propdata ) {
                    $value = $cur_propdata['value'];
                    $valuetype = $cur_propdata['type'];
                    $valuedatatype = $cur_propdata['datatype'];
                    if ( $valuetype == 'uri' ) {
                        $value_uris[$value] = array(
                			'type' => $valuetype, 
                			'datatype' => $valuedatatype 
                        ); 
                    }
                }
            }
        }
        return $value_uris;
    }

    /**
     * Convert an entity type identifier used by ARC, to one used by SMW
     * (Example uri --> Page)
     * @param string $arctype
     * @param string $arcdatatype
     * 
     */
    function convertARCTypeToSMWType( $arctype, $arcdatatype ) {
        if ( $arctype == 'uri' ) {
            return 'Page';
        } else if ( $arctype == 'literal' ) {
            if ( $arcdatatype == 'http://www.w3.org/2001/XMLSchema#decimal' ) {
                return 'Number';
            } else {
                return 'String';
            }
        } else {
            $this->addError('Unknown entity type in SMWBatchWriter.php:convertARCTypeToSMWType');
        }
        // TODO: Expand with more options
    }

    /**
     * Get from the ARC2 parser, an array index, of namespace prefixes, and the
     * corresponding full namespaces.
     * @return array
     */
    function getNSPrefixMapping() {
        return $this->m_parser->nsp;
    }

    /**
     * Cleanup some unfortunate combination of tags and linebreaks, that breaks the ARC2 parser
     * @param string $xmldata
     * @return boolean
     */
    private function cleanupXML( $xmldata ) {
        // Cleaning up unfortunate line breaks, which tend to bread RDFXML parser
        $xmldata = str_replace( ">\r\n", '>', $xmldata );
        $xmldata = str_replace( ">\n", '>', $xmldata );
        return $xmldata;
    }
    
    private function addError( $errormessage ) {
        $this->m_errors[count($this->m_errors)] = $errormessage;
        $this->m_haserrors = true; 
    }

    public function hasErrors() {
        return $this->m_haserrors; 
    }
    

    /**
     * Get an array of current errors
     * @return array
     */
    public function getErrors( ) {
        return $this->m_errors;
    }
    
    /**
     * Get a textual representation of current errors
     * @return string $errortext
     */
    public function getErrorText() {
        $errors = $this->m_errors;
        $errortext = '';
        $i = 1;
        foreach ( $errors as $error ) {
            $errortext .= "$error\n";
            $i++;
        }
        return $errortext; 
    }
}



class RDFIOPageHandler {
    protected $m_wikititle;
    protected $m_wikititlefull;
    protected $m_ns;
    protected $m_properties;
    protected $m_edittoken;
    protected $m_smwwriter;
    protected $m_smwwriter_add;
    protected $m_smwwriter_remove;
    protected $m_equivurihandler;
    protected $m_errors;
    protected $m_haserrors;

    function __construct( $wikititle, $ns, $properties, $equivurihandler ) {
        $this->m_wikititle = $wikititle;
        $this->m_ns = $ns;
        if ( $ns == SMW_NS_PROPERTY ) {
            $this->m_wikititlefull = 'Property:' . $wikititle;
        } else {
            $this->m_wikititlefull = $wikititle;
        }
        $this->m_properties = $properties;
        $this->m_equivurihandler = $equivurihandler;
        $this->m_haserrors = false;
    }

    public function writeDataToWiki( $delete = false ) {
        if ( $delete ) {
            if ( $this->checkWikiPageExists() ) {
                $this->initSMWWriter( $delete = true );
            } else {
                return;
            }
        } else {
            $this->ensureWikiPageExists();
            $this->initSMWWriter();
        }

        $properties = $this->m_properties;
        foreach ( $properties as $cur_prop ) {
            $propertystring = $cur_prop['p'];
            $property = SMWPropertyValue::makeUserProperty( $propertystring );
            $valuestring = $this->m_equivurihandler->sanitizeSMWValue( $cur_prop['v'] );
            $value    = SMWDataValueFactory::newPropertyObjectValue( $property, $valuestring );

            $propertyErrorText = $property->getErrorText();
            $propertyHasError = ( $propertyErrorText != '' );
            if ( $propertyHasError ) {
                $this->addError( "<p>In RDFIOPageHandler::writeDataToWiki(): " . $property->getErrorText() . "</p>");
            }
            
            $valueErrorText = $value->getErrorText();
            $valueHasError = ( $valueErrorText != '' );
            if ( $valueHasError ) {
                $this->addError( "<p>Error creating property value object in RDFIOPageHandler::writeDataToWiki():</p><p>" . $value->getErrorText() . "</p>");
            }
            if ( $delete ) {
                $this->m_smwwriter_remove->addPropertyObjectValue( $property, $value );
                $editmessage = "Deleting properties. Last property delete: " . $propertystring . " : " . $valuestring;
            } else {
                $this->m_smwwriter_add->addPropertyObjectValue( $property, $value );
                $editmessage = "Importing properties. Last property added: " . $propertystring . " : " . $valuestring;
            }
        }
        
        $this->m_smwwriter->update( $this->m_smwwriter_remove, $this->m_smwwriter_add, $editmessage );
        $smwWriterError = $this->m_smwwriter->getError();
        $smwWriterHasError = ( $smwWriterError != '' );
        if ( $smwWriterHasError ) {
            $this->addError( "<p>SMWWriter Error: " . $smwWriterError . "</p>" );
        }
    }
    
    public function deleteDataFromWiki() {
        $this->writeDataToWiki( $delete = true );
    }

    private function checkWikiPageExists() {
        $this->m_wikipageexists = $this->wikiPageExistsSaveEditToken();
        return $this->m_wikipageexists;
    }
    
    /**
     * For a wiki page title, check if it exists (and at the same time, store an edit token),
     * and if not, create it.
     * @param string $wikititle
     */
    private function ensureWikiPageExists() {
        $this->m_wikipageexists = $this->wikiPageExistsSaveEditToken();
        if ( !$this->m_wikipageexists ) {
            $this->createWikiPage();
        }
    }

    /**
     * For a wiki page title, create it without writing any content to it.
     * @param string $wikititle
     */
    private function createWikiPage() {
        // Prepare a 'fake' request to the MediaWiki API, which we use internally
        // See http://www.mediawiki.org/wiki/API:Calling_internally for more info
        $fauxEditRequest = new FauxRequest(array (
                        'action' => 'edit',
                        'title' => $this->m_wikititlefull, // For the faux request, the namespace must be included in text form 
                        'section' => '0',
                        'summary' => 'New Page (by SMWRDFConnector)',
                        'text' => '<!-- Empty page -->',
                        'token' => $this->m_edittoken
        ));
        $editApi = new ApiMain($fauxEditRequest, $enableWrite = true);
        $editApi->execute();
        $editApiOutput = & $editApi->getResultData(); // TODO: Take care of this
    }

    /**
     * Checks whether a wiki article with the given title exists. It also receives
     * an edit token for this article and stores in a class variable.
     * @param unknown_type $wikiPageTitle
     * @return boolean
     */
    private function wikiPageExistsSaveEditToken() {
        $fauxRequest = new FauxRequest(array (
                    'action' => 'query',
                    'prop' => 'info',
                    'intoken' => 'edit',
                    'titles' => $this->m_wikititlefull
        ));
        // We are using the MediaWiki API internally.
        // See http://www.mediawiki.org/wiki/API:Calling_internally
        // for more info
        $api = new ApiMain($fauxRequest);
        $api->execute();
        $apioutput = & $api->getResultData(); // TODO: Take care of this
        $apioutputpages = $apioutput['query']['pages'];
        foreach ( $apioutputpages as $page ) {
            $this->m_edittoken = $page['edittoken'];
        }
        // Using intricacies of array structure to determine if page exists
        // TODO: Use more robust method
        $pageismissing = count($apioutput['query']['pages'][-1]) > 0;
        return !$pageismissing;
    }

    private function initSMWWriter( $delete = false ) {
        // Create add and remove objects, to use in SMWWriter calls
        $page = SMWWikiPageValue::makePage( $this->m_wikititle, $this->m_ns );
        $this->m_smwwriter = new SMWWriter( $page->getTitle() );
        if ( $delete ) {
            // We are not adding anything, so create a "page" with "false" title
            $this->m_smwwriter_add    = new SMWSemanticData( SMWWikiPageValue::makePage( false, $this->m_ns ) );
            $this->m_smwwriter_remove = new SMWSemanticData( $page );
        } else {
            $this->m_smwwriter_add    = new SMWSemanticData( $page );
            // We are not removing anything, so create a "page" with "false" title
            $this->m_smwwriter_remove = new SMWSemanticData( SMWWikiPageValue::makePage( false, $this->m_ns ) );
        }
    }
    
    public function getWikiTitleFull() {
        return $this->m_wikititlefull;
    }

    private function addError( $errormessage ) {
        $this->m_errors[count($this->m_errors)] = $errormessage;
        $this->m_haserrors = true; 
    }
    
    public function hasErrors() {
        return $this->m_haserrors; 
    }
    
    public function getErrors( ) {
        return $this->m_errors;
    }
    
    public function getErrorText() {
        $errors = $this->m_errors;
        $errortext = '';
        $i = 1;
        foreach ( $errors as $error ) {
            $errortext .= "$error\n";
            $i++;
        }
        return $errortext; 
    }
}
